package com.googlecode.lightest.core

import org.codehaus.groovy.control.ConfigurationException 
import org.codehaus.groovy.runtime.StackTraceUtils

/**
 * Represents a configuration of the test run
 */
class Configuration implements IConfiguration {
    static final UNSPECIFIED_PREFS = [:] as IPreferences
    static final UNSPECIFIED_ENVS = [
        new TestEnvironment('unspecified1'),
        new TestEnvironment('unspecified2'),
        new TestEnvironment('unspecified3')
    ]
    
    static final DEFAULT_REPORTER =
        'com.googlecode.lightest.core.LightestReporter'
    static final DEFAULT_STRATEGY =
        'com.googlecode.lightest.core.SimpleDispatcherAssignmentStrategy'

    def classLoader
    def classPaths
    def outputDir
    def prefs
    def envs
    def reporter
    def strategy

    Configuration() {
        classLoader = new GroovyClassLoader(this.class.classLoader)
    }

    /**
     * Initializes the configuration.
     *
     * @param configText
     */
    void init(String configText) {
        def config

        if (configText =~ /\S/) {
            def header = 'def config = new NodeBuilder()\n'

            try {
                config = Eval.me(header + configText)
            }
            catch (e) {
                println 'Caught exception parsing the following configuration:'
                println configText
                StackTraceUtils.sanitize(e)
                throw e
            }
        }
        else {
            config = new Node(null, 'config')
        }

        setClassPaths(config)
        setOutputDir(config)
        setPreferences(config)
        setEnvironments(config)
        setReporter(config)
        setDispatcherAssignmentStrategy(config)
    }

    /**
     * Determines if the specified configuration is a valid one. If not, throws
     * an exception.
     *
     * @param config  the configuration to validate
     */
    void validate() {
        // TODO - implement
    }

    private void setClassPaths(Node config) {
        classPaths = [ '.' ]

        if (config.classPaths.size() > 0) {
            config.classPaths[0].path.each {
                classPaths << it.value()
            }
        }
    }

    private void setOutputDir(Node config) {
        outputDir = config.outputDir[0]?.value() ?: 'lightest-report'
        outputDir = new File(outputDir).getCanonicalPath()
    }

    /**
     * Initializes the preferences instance according to the configuration. If
     * the configuration references a property that is not a property of the
     * preference, an exception is thrown.
     *
     * @param config  the configuration containing the preference information.
     *                It must have a "prefs" child with a "class" attribute.
     */
    private void setPreferences(Node config) {
        if (config.prefs.size() == 0) {
            prefs = UNSPECIFIED_PREFS
            return
        }

        assert config.prefs[0].'@class' != null

        prefs = loadClass(config.prefs.'@class').newInstance()

        config.prefs[0].children().each {
            prefs."${it.name()}" = it.value()
        }
    }

    private void setEnvironments(config) {
        if (config.envs.size() == 0 || config.envs.env.size() == 0) {
            envs = UNSPECIFIED_ENVS
            return
        }

        assert config.envs[0].'@class' != null

        envs = []

        def envClass = loadClass(config.envs.'@class')

        for (envConfig in config.envs[0].env) {
            validateEnvironment(envConfig, envs)
            
            def env = envClass.newInstance()
            
            env.setId(envConfig.'@id')

            envConfig.children().each {
                env."${it.name()}" = it.value()
            }

            envs << env
        }
    }
    
    // TODO - replace validation with groovytools-builder metabuilder schema
    private void validateEnvironment(envConfig, envs) {
        def id = envConfig.'@id'
        
        if (! id) {
            def msg = ("The 'id' attribute must be specified for the declared "
                + "environment ${envConfig}")
            throw new ConfigurationException(msg)
        }
        else if (envs.find { it.getId() == id }) {
            def msg = "The id '${id}' is not unique among declared environments"
            throw new ConfigurationException(msg)
        }
    }

    private void setReporter(config) {
        def reporterClass = config.reporter.'@class'
        reporter = loadClass(reporterClass ?: DEFAULT_REPORTER).newInstance()
    }

    private void setDispatcherAssignmentStrategy(config) {
        def strategyClass = config.dispatcherAssignmentStrategy.'@class'
        strategy = loadClass(strategyClass ?: DEFAULT_STRATEGY).newInstance()
    }

    /**
     * Loads the class using a GroovyClassLoader whose classpath is set to
     * include all paths that have been parsed from the configuration text.
     *
     * @param className
     */
    private Class loadClass(String className) {
        classPaths.each { path ->
            def url = new File(path).getCanonicalFile().toURL()
            if (! classLoader.getURLs().find { it == url }) {
                classLoader.addURL(url)
            }
        }

        return classLoader.loadClass(className, true)
    }

    /**
     * Returns the class loader used to load the configuration-related classes.
     */
    GroovyClassLoader getClassLoader() {
        return classLoader
    }

    /**
     * Returns a List of Strings representing class paths under which tasks
     * and potentially environment and preference classes are defined. The
     * current directory is added by default.
     */
    def getClassPaths() {
        return classPaths
    }

    /**
     * Returns the output directory for the test run report. The default output
     * directory is "lightest-report".
     */
    def getOutputDir() {
         return outputDir
     }

    /**
     * Returns an instance of the concrete implementation of IPreferences.
     */
    def getPreferences() {
        return prefs
    }

    /**
     * Returns a List of ITestEnvironment instances. The UNSPECIFIED_ENVS list
     * containing three default environment entries will be returned if
     * unspecified.
     */
    def getEnvironments() {
        return envs
    }

    /**
     * Returns an instance of the concrete subclass of LightestTestListener.
     * A DEFAULT_REPORTER will be returned if unspecified.
     */
    LightestTestListener getReporter() {
        return reporter
    }

    /**
     * Returns the specified dispatcher assignment strategy. A DEFAULT_STRATEGY
     * will be returned if unspecified.
     */
    IDispatcherAssignmentStrategy getDispatcherAssignmentStrategy() {
        return strategy
    }
}
